/*******************************************************************************
 * The MOSES project, model based software engineering of networked embedded systems.
 * 
 * Copyright (C) 2010-2013, University of Applied Sciences Wildau,
 * Telematics Research Group, http://www.tm.th-wildau.de/research-group
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses>.
 * 
 * Contributors:
 *     University of Applied Sciences Wildau - initial API and implementation
 * 
 * If you are interested in a commercial license please contact us via 
 * e-mail: ralf.vandenhouten@th-wildau.de.
 ******************************************************************************/
package de.thwildau.tm.moses.abnf.io;

import java.io.CharConversionException;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CodingErrorAction;
import java.util.List;

/**
 * A coder class that provides static method for encoding and decoding ABNF data.
 *
 * @author Thomas Kistel
 * @version $Revision: 762 $
 */
public class AbnfCoder {
    
    private static final byte[] EMPTY = new byte[0];
    private static final ByteBuffer EMPTY_BUFFER = ByteBuffer.wrap(EMPTY);
	private static final Charset CHAR_SET = Charset.forName("US-ASCII");
	
	/**
     * Encodes the given list of {@link Transmitable} objects to the DataOutput {@code out}.
     *  
     * @param out the DataOutput
     * @param str the list of {@link Transmitable} objects
     * @throws IOException as specified in {@link DataOutput#write(byte[])}
     */
	public static void encode(DataOutput out, List<? extends Transmitable> list) throws IOException {
	    for (Transmitable t : list) {
            t.write(out);
        }
    }
	
	/**
	 * Encodes the given Integer value to the DataOutput {@code out}.
	 *  
	 * @param out the DataOutput
	 * @param str the Integer value that is encoded according to the ABNF standard
	 * @throws IOException as specified in {@link DataOutput#write(byte[])}
	 */
	public static void encode(DataOutput out, Integer integer) throws IOException {
	    encode(out, integer.toString());
	}
	
	/**
	 * Encodes the given String {@code str} to the DataOutput {@code out}.
	 *  
	 * @param out the DataOutput
	 * @param str the String that is encoded according to the ABNF standard
	 * @throws IOException as specified in {@link DataOutput#write(byte[])}
	 */
	public static void encode(DataOutput out, String str) throws IOException {
	    byte[] b = toByteArray(str);
        out.write(b);
	}
	
	private static byte[] toByteArray(String str) throws CharConversionException {
	    try {
	        if(str == null)
	            return EMPTY;
//	        ByteBuffer byteBuffer = CHAR_SET.encode(str);
	        ByteBuffer byteBuffer = CHAR_SET.newEncoder()
	                                        .onMalformedInput(CodingErrorAction.REPORT)
	                                        .onUnmappableCharacter(CodingErrorAction.REPORT)
	                                        .encode(CharBuffer.wrap(str));
	        return byteBuffer.array();
	    } catch(CharacterCodingException e) {
	        CharConversionException cce = new CharConversionException("Could not encode " + str);
	        cce.addSuppressed(e);
	        throw cce;
	    }
	}
	
	/**
	 * Encodes the given {@code ValueEnum} to the DataOutput {@code out}.
	 * 
	 * @param out the DataOutput
	 * @param enumValue the {@code ValueEnum}
	 * @throws IOException as specified in {@link DataOutput#write(byte[])}
	 */
	public static <T extends Enum<?> & ValueEnum> void encode(DataOutput out, T enumValue) throws IOException {
	    encode(out, enumValue.getValue());
	}
	
	/**
	 * Checks whether the next sequences of bytes of the given DataInput can be parsed to the given String. If yes, the
	 * method does nothing. If not this methods throws a {@code ParseException} that contains the bytes that are read
	 * already from the DataInput from this method. These bytes can be further processed.
	 * <p>
	 * This method should be used when the amount of object elements in the stream is not clear. So this method provides
	 * a mechanism to try to decode the content into a specific object.
	 * </p>
	 * 
	 * @param in the DataInput to read from
	 * @param string the expected String
	 * @throws ParseException if the next sequences of bytes cannot be parsed to the given String {@code string}
	 * @throws IOException if any other IO error occurs
	 */
	public static void tryCheckDecode(DataInput in, String string) throws ParseException, IOException {
	    internalDecode(EMPTY_BUFFER, in, string, true);
	}
	
	/**
	 * Checks whether the next sequences of bytes of the given DataInput can be parsed to the given String. If yes, the
     * method does nothing. If not this methods throws an {@code IOException} that contains a detailed messages
     * on what cannot be parsed. 
     * 
	 * @param in the DataInput to read from
	 * @param string the expected String
	 * @throws IOException 
	 *             - if the next sequences of bytes cannot be parsed to the given String {@code string}. This means
	 *             a corrupt sequence
	 *             - any other IO errors
	 */
	public static void checkDecode(DataInput in, String string) throws IOException {
	    internalDecode(EMPTY_BUFFER, in, string, false);
	}
	
	/**
	 * Checks whether the next sequences of bytes of the given DataInput can be parsed to the given String. If yes, the
     * method does nothing. If not this methods throws an {@code IOException} that contains a detailed messages
     * on what cannot be parsed. 
     * <p>
     * The parameter {@code lastBuffer} contains data that were previously read from DataInput and should also be considered to be parsed.
     * The sequences of bytes from the DataInput {@code in} will be appended to the {@code lastBuffer}.
     * </p>
     * 
	 * @param lastBuffer the bytes that were previously read from the DataInput
	 * @param in the DataInput to read from
	 * @param string the expected String
	 * @throws IOException
	 *             - if the next sequences of bytes cannot be parsed to the given String {@code string}. This means
     *             a corrupt sequence
     *             - any other IO errors
	 */
	public static void checkDecode(ByteBuffer lastBuffer, DataInput in, String string) throws IOException {
	    internalDecode(lastBuffer, in, string, false);
	}
	
	private static void internalDecode(ByteBuffer lastBuffer, DataInput in, String string, boolean tryCheck) throws IOException {
	    int newBufferSize = toByteArray(string).length - lastBuffer.capacity();
	    byte[] newBuffer = new byte[newBufferSize];
        in.readFully(newBuffer);
        ByteBuffer bb = ByteBuffer.allocate(newBufferSize + lastBuffer.capacity());
        bb.put(lastBuffer);
        bb.put(newBuffer);
        // don't forget to flip the Buffer before passing to the decode method
        CharBuffer charBuffer = internalDecode((ByteBuffer) bb.flip());
        String readedString = charBuffer.toString();
        if(!string.equals(readedString)) {
            String message = "'" + readedString + "' is not the expected character sequence '" + string + "'";
            if(tryCheck) {
                // don't forget to flip the Buffer before passing to the Exception
                throw new ParseException(message, (ByteBuffer) bb.flip());
            } else {
                throw new IOException("Parsing Error: " + message);
            }
        }
	}
	
	private static CharBuffer internalDecode(ByteBuffer bb) throws CharConversionException {
	    try {
            CharBuffer result = CHAR_SET.newDecoder()
                                        .onMalformedInput(CodingErrorAction.REPORT)
                                        .onUnmappableCharacter(CodingErrorAction.REPORT)
                                        .decode(bb);
            return result;
        } catch (CharacterCodingException e) {
            CharConversionException cce = new CharConversionException("Could not decode ByteBuffer (capacity=" + bb.capacity() + ")");
            cce.addSuppressed(e);
            throw cce;
        }
	}
	
	private static <T extends Number> T valueOfNumber(Class<T> clazz, String value) throws IOException {
        if(value == null || value.isEmpty())
            return null;
        
        try {
            Method method = clazz.getDeclaredMethod("valueOf", String.class);
            @SuppressWarnings("unchecked")
            T result = (T) method.invoke(null, value);
            return result;
        } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            throw new IOException("Cannot compute the " + clazz.getSimpleName() + " value of " + value + ".", e);
        }
    }
	
	/**
	 * Decodes the next sequences of bytes from the given DataInput {@code in} until the delimiter occurred. The bytes
     * read are converted into a String according to the ABNF standard. The delimiter is not returned.
     * <p>
     * Additionally to {@link #decodeUntil(DataInput, String)}, this method converts the string value into the target number
     * class object. When the string value that was read from the DataInput is <code>null</code> or empty, then this method
     * returns <code>null</code>
     * </p>
	 * 
	 * @param in the DataInput to read from
	 * @param delim the delimiter
	 * @param targetClass the target number class
	 * @return the string that is converted into the target number class, or <code>null</code> if an empty string was read
	 * @throws IOException
	 */
	public static <T extends Number> T decodeUntil(DataInput in, String delim, Class<T> targetClass) throws IOException {
	    String result = decodeUntil(in, delim);
	    return valueOfNumber(targetClass, result);
	}
	
	/**
	 * Decodes the next sequences of bytes from the given DataInput {@code in} until the delimiter occurred. The bytes
	 * read are converted into a String according to the ABNF standard. The delimiter is not returned.
	 * 
	 * @param in the DataInput to read from
	 * @param delim the delimiter
	 * @return the converted string
	 * @throws IOException if any IO error occurs
	 */
	public static String decodeUntil(DataInput in, String delim) throws IOException {
	    String str = "";
	    do {
	        byte value = in.readByte();
	        str += decode(value);
	    } while(!str.endsWith(delim));
	    return str.substring(0, (str.length() - delim.length()));
	}
	
	private static String decode(byte[] bytes) throws CharConversionException {
	    ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
        CharBuffer charBuffer = internalDecode(byteBuffer);
	    return charBuffer.toString();
	}
	
	private static String decode(byte b) throws CharConversionException {
	    return decode(new byte[] {b});
	}
	
	/**
	 * Decodes the number of bytes into a String.
	 * 
	 * @param in the DataInput to read from
	 * @param length the number of bytes to read
	 * @return the converted string
	 * @throws IOException if any IO error occurs
	 */
	public static String decode(DataInput in, int length) throws IOException {
        byte[] buf = new byte[length];
        for(int i=0; i<length; i++) {
            buf[i] = in.readByte();
        }
        return decode(buf);
    }
	
	/**
	 * Decodes the number of bytes into a String. 
	 * <p>
     * Additionally to {@link #decode(DataInput, int)}, this method converts the string value into the target number
     * class object. When the string value that was read from the DataInput is <code>null</code> or empty, then this method
     * returns <code>null</code>
     * </p>
     * 
	 * @param in the DataInput to read from
	 * @param length the number of bytes to read
	 * @param targetClass the target number class
	 * @return the string that is converted into the target number class, or <code>null</code> if an empty string was read
	 * @throws IOException if any IO error occurs
	 */
	public static <T extends Number> T decode(DataInput in, int length, Class<T> targetClass) throws IOException {
	    String result = decode(in, length);
	    return valueOfNumber(targetClass, result);
	}
	
	/**
	 * Decodes the next sequences of bytes from the given DataInput {@code in} into a Enum that matches one of the given enum-Values.
	 * <p>
	 * This method aborts reading until the first enum-value matches.
	 * FIXME: This can result in parsing errors when the given enum-values have the same pre-/infix (e.g. 'window' and 'windower' are 
	 * both parsed to 'window'; the end 'er' in the latter case is not considered any more). 
	 * </p>
	 * 
	 * @param in the DataInput to read from
	 * @param allValues the Values of the Enum class (typically by passing {@code enum.values()}.
	 * @return the parsed enum value
	 * @throws IOException
	 *             - if the next bytes cannot be parsed into one of the given enum-values
	 *             - if any IO errors occurs
	 */
	public static <T extends Enum<?> & ValueEnum> T decode(DataInput in, T[] allValues) throws IOException {
	    String str = "";
        T decodedEnum = null;
        do {
            byte value = in.readByte();
            str += decode(value);
            decodedEnum = match(str, allValues);
        } while (decodedEnum == null);
        return decodedEnum;
	}
	
	private static <T extends Enum<?> & ValueEnum> T match(String string, T[] enumValues) throws IOException {
	    if(enumValues.length == 0) // this should never happen
	        throw new IllegalArgumentException();
	    if(string == null)
            return null;
        
	    for(T enumValue : enumValues) {
	        String value = enumValue.getValue();
	        if(value.equals(string))
	            return enumValue;
	        else if(value.startsWith(string))
	            return null;
	    }
	    
	    throw new IOException(string + " cannot be parsed to " + enumValues[0].getClass().getSimpleName());
    }
}
